package nullInvader.View;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FlowLayout;
import java.awt.event.ActionListener;
import java.awt.event.MouseListener;
import java.awt.image.BufferedImage;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.ToolTipManager;

import nullInvader.Control.GameControl;

/**
 * An object of this class represents the GUI of the game. It is
 * responsible for creating each component that a player would want to
 * interact with. It also provides methods for attaching custom
 * ActionListeners to its components.The GUI runs on the Swing thread
 * and its methods should therefore always be accessed from a Swing
 * thread.
 * 
 * @author Emanuel Yifter Lindgren ( dv11 elr)
 * @version 1.0
 */
public class Gui extends JFrame {

    private static final int HEIGHT = 480;
    private static final int WIDTH = 480;
    private static final int MAP_WIDTH = (20 * 20);
    private static final int MAP_HEIGHT = (20 * 20);

    private static final long serialVersionUID = 1L;
    private final JFrame gui = this;

    private final JMenuBar menuBar = new JMenuBar();
    private final JPanel statsPanel = new JPanel();
    private final JPanel sideMenuPanel = new JPanel();

    private final JMenu menuAction = new JMenu("Action");
    private final JMenu menuMisc = new JMenu("Misc");
    private final JMenu menuMap = new JMenu("Map");

    private final JMenuItem menuItemStartGame = new JMenuItem("New Game");
    private final JMenuItem menuItemQuitGame = new JMenuItem("Quit");
    private final JMenuItem menuItemPauseMap = new JMenuItem("Pause");
    private final JMenuItem menuItemRestartMap = new JMenuItem("Restart Level");

    private final JMenuItem menuItemAbout = new JMenuItem("About");
    private final JMenuItem menuItemHelp = new JMenuItem("Help");

    private final JLabel lblHighscore = new JLabel("Highscore: N/A");
    private final JLabel lblScore = new JLabel("Score: N/A");
    private final JLabel lblCash = new JLabel("Cash: N/A");
    private final JLabel lblGoal = new JLabel("Goal: N/A");

    private final GameMapComponent mapDisplayComponent = new GameMapComponent(
            MAP_WIDTH, MAP_HEIGHT);

    private JLabel unit1Label = null;
    private JLabel unit2Label = null;
    private final GameControl controller;

    /**
     * Creates this GUI.
     * 
     * @param controller
     *            an instance of GameControl to which this GUI can
     *            communicate.
     * @throws Exception
     *             Indicating that the gui failed to instantiate
     *             itself.
     */
    public Gui(final GameControl controller) throws Exception {

        this.controller = controller;

        setBounds(100, 100, WIDTH, HEIGHT);
        setResizable(false);
        setTitle("NullInvader");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        getContentPane().setLayout(new BorderLayout(0, 0));
        getContentPane().setBackground(Color.GRAY);

        ToolTipManager.sharedInstance().setInitialDelay(0);
        ToolTipManager.sharedInstance().setDismissDelay(Integer.MAX_VALUE);

        buildMenu();
        buildStatsPanel();
        buildSidePanel();
        buildMapComponent();
    }

    /* sets options and adds the map component to the contentpane. */
    private void buildMapComponent() {

        mapDisplayComponent.setPreferredSize(new Dimension(MAP_WIDTH,
                MAP_HEIGHT));
        getContentPane().add(mapDisplayComponent, BorderLayout.WEST);
        mapDisplayComponent.setBorder(BorderFactory.createRaisedBevelBorder());

    }

    /*
     * constructs the sidemenupanel by getting the label icons and
     * adding each label to the sidemenupanel.
     */
    private void buildSidePanel() throws Exception {

        getContentPane().add(sideMenuPanel, BorderLayout.EAST);
        sideMenuPanel
                .setLayout(new BoxLayout(sideMenuPanel, BoxLayout.Y_AXIS));
        try {
            unit1Label = new JLabel(new ImageIcon(getClass().getResource(
                    "/nullInvader/Resources/Unit1Label.png")));
            unit2Label = new JLabel(new ImageIcon(getClass().getResource(
                    "/nullInvader/Resources/Unit2Label.png")));
        } catch (Exception e) {
            throw new Exception("Missing important game files."
                    + " Unit1Label.png/Unit2Label.png");
        }

        sideMenuPanel.add(unit1Label);
        sideMenuPanel.add(Box.createRigidArea((new Dimension(50, 50))));
        sideMenuPanel.add(unit2Label);
    }

    /*
     * constructs the statspanel by setting options and adding each
     * statlabel to the panel.
     */
    private void buildStatsPanel() {

        FlowLayout flowLayoutStats = (FlowLayout) statsPanel.getLayout();
        flowLayoutStats.setAlignment(FlowLayout.LEFT);
        flowLayoutStats.setHgap(20);

        getContentPane().add(statsPanel, BorderLayout.NORTH);

        statsPanel.add(lblScore);
        statsPanel.add(lblCash);
        statsPanel.add(lblGoal);
        statsPanel.add(lblHighscore);
    }

    /*
     * constructs the menubar by setting attaching each menuitem to
     * the menubar and disabling access to items that should not be
     * accessed unless a game has started.
     */
    private void buildMenu() {

        setJMenuBar(menuBar);

        menuBar.add(menuAction);
        menuBar.add(menuMisc);
        menuAction.add(menuMap);

        menuMap.add(menuItemPauseMap);
        menuMap.add(menuItemRestartMap);
        menuAction.add(menuItemStartGame);
        menuAction.add(menuItemQuitGame);

        menuMisc.add(menuItemHelp);
        menuMisc.add(menuItemAbout);

        menuMap.setEnabled(false);
        menuItemQuitGame.setEnabled(false);
    }

    /**
     * Attaches an ActionListener to the component thats being
     * interacted with in order to start the game.
     * 
     * @param aL
     *            an ActionListener to be attached.
     */
    public void addGameStartListener(final ActionListener aL) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                menuItemStartGame.addActionListener(aL);
            }
        });
    }

    /**
     * Attaches an ActionListener to the component thats being
     * interacted with in order to quit the game.
     * 
     * @param aL
     *            an ActionListener to be attached.
     */
    public void addGameQuitListener(final ActionListener aL) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                menuItemQuitGame.addActionListener(aL);
            }
        });
    }

    /**
     * Attaches an ActionListener to the component thats being
     * interacted with in order to pause the game/map.
     * 
     * @param aL
     *            an ActionListener to be attached.
     */
    public void addMapPauseListener(final ActionListener aL) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                menuItemPauseMap.addActionListener(aL);
            }
        });
    }

    /**
     * Attaches an ActionListener to the component thats being
     * interacted with in order to restart the map.
     * 
     * @param aL
     *            an ActionListener to be attached.
     */
    public void addMapRestartListener(final ActionListener aL) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                menuItemRestartMap.addActionListener(aL);
            }
        });
    }

    /**
     * Attaches an ActionListener to the component thats being
     * interacted with in order to buy the first unit.
     * 
     * @param mL
     *            an ActionListener to be attached.
     */
    public void addUnit1Listener(final MouseListener mL) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                unit1Label.addMouseListener(mL);
            }
        });
    }

    /**
     * Attaches an ActionListener to the component thats being
     * interacted with in order to buy the second unit.
     * 
     * @param mL
     *            an ActionListener to be attached.
     */
    public void addUnit2Listener(final MouseListener mL) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                unit2Label.addMouseListener(mL);
            }
        });
    }

    /**
     * Attaches a MouseListener to the GUI's Map Component.
     * 
     * @param mL
     *            a MouseListener to be attached.
     */
    public void addMapListener(final MouseListener mL) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                mapDisplayComponent.addMouseListener(mL);
            }
        });
    }

    /**
     * Attaches an ActionListener to the component thats being
     * interacted with in order to display information about the game.
     * 
     * @param aL
     *            an ActionListener to be attached.
     */
    public void addHelpAboutListener(final ActionListener aL) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                menuItemAbout.addActionListener(aL);
            }
        });
    }

    /**
     * Attaches an ActionListener to the component thats being
     * interacted with in order to display game controls.
     * 
     * @param aL
     *            an ActionListener to be attached.
     */
    public void addHelpControlsListener(final ActionListener aL) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                menuItemHelp.addActionListener(aL);
            }
        });
    }

    /**
     * Sets the GUI highscore display.
     * 
     * @param highscore
     *            the current highscore on a map.
     * @param holderNick
     *            the nickname registered with the highscore.
     */
    public void setHighscore(final String highscore, final String holderNick) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                lblHighscore.setText("Highscore: " + highscore + "["
                        + holderNick + "]");
            }
        });
    }

    /**
     * Sets the pause state on the GUI.
     * 
     * @param state
     *            true means paused.
     */
    public void setPausedState(final boolean state) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {

                String stateText = (state) ? "Pause" : "Resume";
                setComponentStates(state);

                menuItemPauseMap.setText(stateText);
            }
        });

    }

    /*
     * disables/enables components that should not be intractable
     * during a game pause.
     */
    private void setComponentStates(boolean state) {
        menuItemStartGame.setEnabled(state);
        menuItemQuitGame.setEnabled(state);
        menuItemRestartMap.setEnabled(state);
        unit1Label.setEnabled(state);
        unit2Label.setEnabled(state);
    }

    /**
     * Changes the game state of this GUI. This is called in order to
     * setup components that behave or look different when the game is
     * actually being played.
     */
    public void setStartedState() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                menuMap.setEnabled(true);
                menuItemQuitGame.setEnabled(true);
                menuItemStartGame.setText("Restart");
            }
        });

    }

    /**
     * Prompts an error dialog to the user.
     * 
     * @param errorMsg
     *            the message to display to the user.
     * @param fatal
     *            true if this error is severe enough to require a
     *            game exit.
     */
    public void promptErrorDialog(final String errorMsg, final boolean fatal) {

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                JOptionPane.showMessageDialog(gui, errorMsg, "Error!",
                        JOptionPane.ERROR_MESSAGE);
                if (fatal) {
                    controller.quitGame();
                }

            }
        });

    }

    /**
     * Asks the user if he wants to submit the highscore to the
     * database. If the required input is empty or the dialog is
     * cancelled. The score wont be submitted.
     * 
     * @param highscore
     *            the score that was a new highscore.
     */
    public void promptHighscoreSubmitDialog(final int highscore) {

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                String answer = JOptionPane.showInputDialog(gui,
                        "Would you like to submit your highscore?"
                                + " Please supply a nickname.",
                        "New Highscore!! " + highscore,
                        JOptionPane.QUESTION_MESSAGE);

                if (answer != null && answer.length() > 0) {
                    controller.highscoreResponse(answer, highscore);
                }

            }
        });

    }

    /**
     * This method creates a dialog asking the player what action he
     * would like to take upon winning a map. The options are
     * "Restart this Map" or "Go to Next". The answer is then sent to
     * the GameControl object assigned to the GUI.
     * 
     */
    public void promptWinDialog() {

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                Object[] options = { "Restart this Map!", "Go to Next!" };
                int answer = JOptionPane.showOptionDialog(gui,
                        "What would you like to do?", "You Won!",
                        JOptionPane.YES_NO_OPTION,
                        JOptionPane.QUESTION_MESSAGE, null, options,
                        options[0]);
                if (answer == JOptionPane.YES_OPTION) {
                    controller.winResponse(true);
                } else {
                    controller.winResponse(false);
                }
            }
        });

    }

    /**
     * This method creates a dialog asking the player what action he
     * would like to take upon losing the game. The options are
     * "Restart the Game" and "Quit Game". The answer is then sent to
     * the GameControl object assigned to the GUI.
     * 
     */
    public void promptLossDialog() {

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                Object[] options = { "Restart Game!", "Quit Game" };
                int answer = JOptionPane.showOptionDialog(gui,
                        "What would you like to do?", "You Lost!",
                        JOptionPane.YES_NO_OPTION,
                        JOptionPane.QUESTION_MESSAGE, null, options,
                        options[0]);
                if (answer == JOptionPane.YES_OPTION) {
                    controller.lostResponse(true);
                } else {
                    controller.lostResponse(false);
                }

            }
        });

    }

    /**
     * Opens a dialog with the player upon finishing the game. This
     * method should be called when there is no more maps left. The
     * dialog options are "Restart Game" and "Quit Game". The answer
     * is then sent back to the GameControl object attached to this
     * GUI.
     * 
     */
    public void promptGameFinishedDialog() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                Object[] options = { "Restart Game!", "Quit Game" };
                int answer = JOptionPane.showOptionDialog(gui,
                        "Congratulations you finished the game!", "Hurray!!",
                        JOptionPane.YES_NO_OPTION,
                        JOptionPane.QUESTION_MESSAGE, null, options,
                        options[0]);
                if (answer == JOptionPane.YES_OPTION) {
                    /*
                     * The actions taken are the same as for a lost
                     * game.
                     */
                    controller.lostResponse(true);
                } else {
                    controller.lostResponse(false);
                }

            }
        });

    }

    /**
     * Sets the tooltip information to be displayed. The tooltips are
     * shown when the played moves the mouse over the unit icons in
     * the GUI.
     * 
     * @param unit1Name
     *            the name of the first unit.
     * @param unit2Name
     *            the name of the second unit.
     * @param unit1Price
     *            the price of the first unit.
     * @param unit2Price
     *            the price of the second unit.
     */
    public void setUnitTooltip(final String unit1Name, final String unit2Name,
            final int unit1Price, final int unit2Price) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                unit1Label
                        .setToolTipText("<html><font color=\"Green\"><b>Name:"
                                + " </b></font>" + unit1Name
                                + "<font color=\"Green\"><b><br>Price: "
                                + " </b></font>" + unit1Price + "$</html>");
                unit2Label
                        .setToolTipText("<html><font color=\"Green\"><b>Name:"
                                + " </b></font>" + unit2Name
                                + "<font color=\"Green\"><b><br>Price:"
                                + " </b></font>" + unit2Price + "$</html>");

            }
        });

    }

    /**
     * Updates the player statistics display.
     * 
     * @param cash
     *            the amount of cash the player has currently.
     * @param score
     *            the amount of score the player has currently.
     * @param goal
     *            the amount of units that need to reach the goal to
     *            win the map.
     */
    public void updateStats(final int cash, final int score, final int goal) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                lblCash.setText("Cash: " + cash + "$");
                lblScore.setText("Score: " + score);
                lblGoal.setText("Goal: " + goal);

            }
        });
    }

    /**
     * Paints the map components image buffer. A call to this method
     * will result in the buffer being displayed on the GUI.
     */
    public void repaintMap() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                mapDisplayComponent.repaint();
            }
        });
    }

    /**
     * Draws an image to the game map buffer.
     * 
     * @param image
     *            the image to be drawn.
     * @param x
     *            the x-axis coordinate the drawing should be made
     *            from.
     * @param y
     *            the y-axis coordinate the drawing should be made
     *            from.
     */
    public void drawMapImage(final BufferedImage image, final int x,
            final int y) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                mapDisplayComponent.drawImage(image, x, y);
            }
        });
    }

}
